﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Antlr.Runtime.Tree;
using Microsoft.Linq.Expressions;
using Tokens = ECMAScriptLexer;
using Microsoft.Scripting.Runtime;
using Microsoft.Scripting.Utils;
using System.IO;
using Microsoft.Scripting.Ast;
using Microsoft.Scripting;
using JavascriptEngine.Runtime;

namespace JavascriptEngine.Compiler
{
    internal class AstGenerator
    {
        #region Private Members
        private Expression _rootExpression;
        private CommonTree _rootNode;
        private CompilerContext _context;

        private ContextFrame GlobalContext = new ContextFrame();
        private ContextFrame CurrentContext;
        #endregion

        internal AstGenerator(CompilerContext context, CommonTree root)
        {
            _rootNode = root;
            _context = context;
            CurrentContext = GlobalContext;
        }

        internal LambdaExpression SubGenerate()
        {
            _rootExpression = Traverse(_rootNode);

            _rootExpression = Expression.Lambda<Func<Scope, LanguageContext, object>>(_rootExpression, "module",
                new[] { Expression.Parameter(typeof(Scope), "$scope"),
                    Expression.Parameter(typeof(LanguageContext), "$language")});

            return _rootExpression as LambdaExpression;
        }
        internal Func<Scope, LanguageContext, object> Generate()
        {
            SubGenerate();
            return (_rootExpression as Expression<Func<Scope, LanguageContext, object>>).Compile();
        }

        private Expression Traverse(CommonTree rootNode)
        {
            Expression ret;
            List<Expression> body;
            switch (rootNode.Type)
            {
                case Tokens.VAR:
                    return BuildVarDeclaration(rootNode);
                case Tokens.ASSIGN:
                    return BuildAssignmentStatment(rootNode);
                case Tokens.Identifier:
                    return LookupVarAndUpdateContext(rootNode.Text, true);
                case Tokens.StringLiteral:
                    return Expression.Constant(rootNode.Text, typeof(string));
                case Tokens.DecimalLiteral:
                    JavaScriptObject obj = double.Parse(rootNode.Text);
                    return Expression.Constant(obj, typeof(JavaScriptObject));
                // Binary operators
                case Tokens.ADD:
                    return BuildBinaryOperator(Operators.Add, rootNode.Children[0] as CommonTree, rootNode.Children[1] as CommonTree);
                case Tokens.MUL:
                    return BuildBinaryOperator(Operators.Multiply, rootNode.Children[0] as CommonTree, rootNode.Children[1] as CommonTree);
                case Tokens.DIV:
                    return BuildBinaryOperator(Operators.Divide, rootNode.Children[0] as CommonTree, rootNode.Children[1] as CommonTree);
                case Tokens.SUB:
                    return BuildBinaryOperator(Operators.Subtract, rootNode.Children[0] as CommonTree, rootNode.Children[1] as CommonTree);
                
                case Tokens.ARGS:
                    break;
                case Tokens.BLOCK:
                    body = new List<Expression>();
                    if (rootNode.Children != null)
                    {
                        foreach (CommonTree child in rootNode.Children)
                        {
                            body.Add(Traverse(child));
                        }
                    }
                    else
                        body.Add(Expression.Empty());

                    //TODO: must change to only do in function
                    body.Add(Expression.Label(CurrentContext.ReturnLabel, Expression.Constant(null, typeof(JavaScriptObject))));
                    ret = Expression.Block(body);
                    return ret;
                
                // Functions
                case Tokens.FUNCTION:
                    if (rootNode.Children.Count == 3)
                        return BuildFunctionDeclaration((Traverse(rootNode.Children[0] as CommonTree) as ParameterExpression),
                            rootNode.Children[1] as CommonTree,
                            rootNode.Children[2] as CommonTree);
                    else
                        return BuildFunctionDeclaration(Path.GetFileNameWithoutExtension(Path.GetRandomFileName()),
                            rootNode.Children[0] as CommonTree,
                            rootNode.Children[1] as CommonTree);
                case Tokens.RETURN:
                    if (rootNode.Children != null)
                        return Expression.Return(CurrentContext.ReturnLabel,
                        Traverse(rootNode.Children[0] as CommonTree)); // what about Tuple case?
                        
                    break;
                case Tokens.CALL:
                    return BuildMethodCall(rootNode.Text, rootNode.Children);

                case 0:
                    body = new List<Expression>();
                    foreach (CommonTree child in rootNode.Children)
                    {
                        body.Add(Traverse(child));
                    }
                    ret = Expression.Block(GlobalContext.Variables.Values, body);
                    return ret;
            }
            return Expression.Empty();
        }

        private Expression BuildFunctionDeclaration(ParameterExpression funcParam, CommonTree args, CommonTree body)
        {
            CurrentContext = new ContextFrame(CurrentContext);

            List<ParameterExpression> officialParams = new List<ParameterExpression>();
            Expression argsExpr = Traverse(args);
            foreach (CommonTree node in args.Children)
            {
                if (node.Type == Tokens.Identifier)
                {
                    ParameterExpression expr = Expression.Parameter(typeof(JavaScriptObject), node.Text);                    
                    CurrentContext.Add(expr.Name, expr);
                    officialParams.Add(expr);
                }
            }
            LabelTarget retLabel = Expression.Label(typeof(JavaScriptObject), "exitLabel");
            CurrentContext.ReturnLabel = retLabel;
           
            Expression bodyExpr = Traverse(body);

            // Javascript doesn't distinguish between function declaration and variable declaration. 
            // To javascript everything is a variable.
            // this means if you have a function named "x", and created a variable named "x", it will override the 
            // the function.
            ParameterExpression funcExpr;
            if ((funcExpr = CurrentContext.LookUp(funcParam.Name)) == null)
                // javascript has global scope for function names
                GlobalContext.Add(funcParam.Name, funcParam);                
            else
                funcExpr = funcParam;

            
            JavaScriptFunction function = new JavaScriptFunction(funcExpr.Name);
            JavaScriptObject[] arr = new JavaScriptObject[officialParams.Count];
            ////
            ParameterExpression paramsExpre = Expression.Parameter(typeof(JavaScriptObject[]), "arguments");
            Expression[] arrExpression = new Expression[officialParams.Count + 1];
            for (int i =0; i<officialParams.Count; ++i)
            {
                ParameterExpression formalParam = officialParams[i];
                IndexExpression indexExpression = Expression.ArrayAccess(paramsExpre, Expression.Constant(i));

                arrExpression[i] = Expression.Assign(formalParam, indexExpression);   
            }
            arrExpression[arrExpression.Length - 1] = bodyExpr;
            function.Function = Expression.Lambda(Expression.Block(CurrentContext.Variables.Values, arrExpression), paramsExpre);
            
            Expression ret = Expression.Assign(funcExpr,
                Expression.Constant(function, typeof(JavaScriptFunction)));            
            CurrentContext = CurrentContext.Parent;

            return ret;
        }
        private Expression BuildFunctionDeclaration(string name, CommonTree args, CommonTree body)
        {
            ParameterExpression funcParam = Expression.Parameter(typeof(JavaScriptObject), name);
            return BuildFunctionDeclaration(funcParam, args, body);
        }

        private Expression BuildMethodCall(string method, System.Collections.IList parameters)
        {
            List<Expression> expressions = new List<Expression>();
            List<string> argsNames = new List<string>();
            if (parameters != null)
            {
                foreach (CommonTree child in (parameters[1] as CommonTree).Children)
                {
                    Expression expression = Traverse(child);
                    expressions.Add(expression);
                    argsNames.Add(Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));
                }
            }
            string functionName = (parameters[0] as CommonTree).Text;
            ParameterExpression expr = CurrentContext.LookUp(functionName);
            //<TODO> this is wrong, we should be smart and delegate the binding to RT to bind to .Net stuff </TODO>
            if (null == expr && functionName.Equals("print", StringComparison.OrdinalIgnoreCase))
                return Expression.Call(typeof(JSUtils).GetMethod("WriteLine_" + expressions.Count), expressions);
            else if (expr == null)
            {
                // add a new function 
                expr = Expression.Parameter(typeof(JavaScriptFunction), functionName);
                GlobalContext.Add(functionName, expr );// function names has global context.
            }
            
            {                
                expressions.Insert(0, expr);
                return Expression.Dynamic(
                    new JavascriptOperation(
                        new JavascriptBinder(_context.SourceUnit.LanguageContext.DomainManager),
                        expressions.Count - 1, argsNames.ToArray()), 
                    typeof(JavaScriptObject), expressions);
            }
        }

        private Expression BuildBinaryOperator(Operators oper, CommonTree leftChild, CommonTree rightChild)
        {
            return Expression.Dynamic(new BinaryOperation(oper, 
                new JavascriptBinder(_context.SourceUnit.LanguageContext.DomainManager)), 
                typeof(JavaScriptObject), Traverse(leftChild), Traverse(rightChild));
        }

        private Expression BuildVarDeclaration(CommonTree tree)
        {
            if (tree.Children != null)
            {
                List<Expression> exprs = new List<Expression>();
                foreach (CommonTree child in tree.Children)
                {
                    if (child.Type == Tokens.Identifier)
                    {
                        ParameterExpression variable = LookupVarAndUpdateContext(child.Text, false);
                    }
                    else if (child.Type == Tokens.ASSIGN)
                    {
                        exprs.Add(BuildAssignmentStatment(child));
                    }
                }
                if (exprs.Count != 0)
                    return BlockExpression.Block(exprs);
            }
            return Expression.Empty();
        }

        private Expression BuildAssignmentStatment(CommonTree child)
        {
            Assert.IsTrue(() => child.Children.Count == 2);
            CommonTree lhs = child.Children[0] as CommonTree;
            CommonTree rhs = child.Children[1] as CommonTree;

            // lookup lhs 
            ParameterExpression var = LookupVarAndUpdateContext(lhs.Text, true);
            Expression exprRHS = Traverse(rhs);
            return Expression.Assign(var, exprRHS);

        }

        private ParameterExpression LookupVarAndUpdateContext(string p, bool addToGlobal)
        {
            ParameterExpression value  = CurrentContext.LookUp(p);
            if (value == null)
            {
                if (addToGlobal)
                    value = GlobalContext.Add(p);
                else
                    value = CurrentContext.Add(p);
            }
            return value;          
        }
    }
}
